Passed
Pull Request — master (#127)
by
unknown
01:55
created

index.js ➔ update   D

Complexity

Conditions 13

Size

Total Lines 52
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 34
dl 0
loc 52
rs 4.2
c 0
b 0
f 0
cc 13

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like index.js ➔ update often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
const fs = require('fs')
2
const ID3Definitions = require("./src/ID3Definitions")
3
const ID3Util = require('./src/ID3Util')
4
const ID3Helpers = require('./src/ID3Helpers')
5
const { isFunction, isString } = require('./src/util')
6
7
/*
8
**  Used specification: http://id3.org/id3v2.3.0
9
*/
10
11
/**
12
 * Checks and removes already written ID3-Frames from a buffer
13
 * @param {Buffer} data
14
 * @returns {boolean|Buffer}
15
 */
16
function removeTagsFromBuffer(data) {
17
    const framePosition = ID3Util.getFramePosition(data)
18
19
    if (framePosition === -1) {
20
        return data
21
    }
22
23
    const encodedSize = data.slice(framePosition + 6, framePosition + 10)
24
    if (!ID3Util.isValidEncodedSize(encodedSize)) {
25
        return false
26
    }
27
28
    if (data.length >= framePosition + 10) {
29
        const size = ID3Util.decodeSize(encodedSize)
30
        return Buffer.concat([
31
            data.slice(0, framePosition),
32
            data.slice(framePosition + size + 10)
33
        ])
34
    }
35
36
    return data
37
}
38
39
function writeInBuffer(tags, buffer, fn) {
40
    buffer = removeTagsFromBuffer(buffer) || buffer
41
    const completeBuffer = Buffer.concat([tags, buffer])
42
    if(isFunction(fn)) {
43
        fn(null, completeBuffer)
44
        return undefined
45
    }
46
    return completeBuffer
47
}
48
49
function writeAsync(tags, filename, fn) {
50
    try {
51
        fs.readFile(filename, (error, data) => {
52
            if(error) {
53
                fn(error)
54
                return
55
            }
56
            data = removeTagsFromBuffer(data) || data
57
            const newData = Buffer.concat([tags, data])
58
            fs.writeFile(filename, newData, 'binary', (error) => {
59
                fn(error)
60
            })
61
        })
62
    } catch(error) {
63
        fn(error)
64
    }
65
}
66
67
function writeSync(tags, filename) {
68
    try {
69
        let data = fs.readFileSync(filename)
70
        data = removeTagsFromBuffer(data) || data
71
        const newData = Buffer.concat([tags, data])
72
        fs.writeFileSync(filename, newData, 'binary')
73
    } catch(error) {
74
        return error
75
    }
76
    return true
77
}
78
79
/**
80
 * Write passed tags to a file/buffer
81
 * @param tags - Object containing tags to be written
82
 * @param filebuffer - Can contain a filepath string or buffer
83
 * @param fn - (optional) Function for async version
84
 * @returns {boolean|Buffer|Error}
85
 */
86
function write(tags, filebuffer, fn) {
87
    const completeTags = create(tags)
88
89
    if(filebuffer instanceof Buffer) {
90
        return writeInBuffer(completeTags, filebuffer, fn)
91
    }
92
    if(isFunction(fn)) {
93
        return writeAsync(completeTags, filebuffer, fn)
94
    }
95
    return writeSync(completeTags, filebuffer)
96
}
97
98
/**
99
 * Creates a buffer containing the ID3 Tag
100
 * @param tags - Object containing tags to be written
101
 * @param fn fn - (optional) Function for async version
102
 * @returns {Buffer}
103
 */
104
function create(tags, fn) {
105
    const frames = ID3Helpers.createBufferFromTags(tags)
106
107
    //  Create ID3 header
108
    const header = Buffer.alloc(10)
109
    header.fill(0)
110
    header.write("ID3", 0)              //File identifier
111
    header.writeUInt16BE(0x0300, 3)     //Version 2.3.0  --  03 00
112
    header.writeUInt16BE(0x0000, 5)     //Flags 00
113
    ID3Util.encodeSize(frames.length).copy(header, 6)
114
115
    const id3Data = Buffer.concat([header, frames])
116
117
    if(isFunction(fn)) {
118
        fn(id3Data)
119
        return undefined
120
    }
121
    return id3Data
122
}
123
124
function readSync(filebuffer, options) {
125
    if(isString(filebuffer)) {
126
        filebuffer = fs.readFileSync(filebuffer)
127
    }
128
    return ID3Helpers.getTagsFromBuffer(filebuffer, options)
129
}
130
131
function readAsync(filebuffer, options, fn) {
132
    if(isString(filebuffer)) {
133
        fs.readFile(filebuffer, (error, data) => {
134
            if(error) {
135
                fn(error, null)
136
            } else {
137
                fn(null, ID3Helpers.getTagsFromBuffer(data, options))
138
            }
139
        })
140
    } else {
141
        fn(null, ID3Helpers.getTagsFromBuffer(filebuffer, options))
142
    }
143
}
144
145
/**
146
 * Read ID3-Tags from passed buffer/filepath
147
 * @param filebuffer - Can contain a filepath string or buffer
148
 * @param options - (optional) Object containing options
149
 * @param fn - (optional) Function for async version
150
 * @returns {boolean}
151
 */
152
function read(filebuffer, options, fn) {
153
    if(!options || typeof options === 'function') {
154
        fn = fn || options
155
        options = {}
156
    }
157
    if(isFunction(fn)) {
158
        return readAsync(filebuffer, options, fn)
159
    }
160
    return readSync(filebuffer, options)
161
}
162
163
/**
164
 * Update ID3-Tags from passed buffer/filepath
165
 * @param tags - Object containing tags to be written
166
 * @param filebuffer - Can contain a filepath string or buffer
167
 * @param options - (optional) Object containing options
168
 * @param fn - (optional) Function for async version
169
 * @returns {boolean|Buffer|Error}
170
 */
171
function update(tags, filebuffer, options, fn) {
172
    if(!options || typeof options === 'function') {
173
        fn = fn || options
174
        options = {}
175
    }
176
177
    const rawTags = Object.keys(tags).reduce((acc, val) => {
178
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
179
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
180
        } else {
181
            acc[val] = tags[val]
182
        }
183
        return acc
184
    }, {})
185
186
    const updateFn = (currentTags) => {
187
        currentTags = currentTags.raw || {}
188
        Object.keys(rawTags).map((specName) => {
189
            const options = ID3Util.getSpecOptions(specName, 3)
190
            const cCompare = {}
191
            if(options.multiple && currentTags[specName] && rawTags[specName]) {
192
                if(options.updateCompareKey) {
193
                    currentTags[specName].forEach((cTag, index) => {
194
                        cCompare[cTag[options.updateCompareKey]] = index
195
                    })
196
197
                }
198
                if (!(rawTags[specName] instanceof Array)) {
199
                    rawTags[specName] = [rawTags[specName]]
200
                }
201
                rawTags[specName].forEach((rTag) => {
202
                    const comparison = cCompare[rTag[options.updateCompareKey]]
203
                    if (comparison !== undefined) {
204
                        currentTags[specName][comparison] = rTag
205
                    } else {
206
                        currentTags[specName].push(rTag)
207
                    }
208
                })
209
            } else {
210
                currentTags[specName] = rawTags[specName]
211
            }
212
        })
213
214
        return currentTags
215
    }
216
217
    if(!isFunction(fn)) {
218
        return write(updateFn(read(filebuffer, options)), filebuffer)
219
    }
220
221
    write(updateFn(read(filebuffer, options)), filebuffer, fn)
222
}
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
223
224
/**
225
 * @param {string} filepath - Filepath to file
226
 * @returns {boolean|Error}
227
 */
228
function removeTagsSync(filepath) {
229
    let data
230
    try {
231
        data = fs.readFileSync(filepath)
232
    } catch(error) {
233
        return error
234
    }
235
236
    const newData = removeTagsFromBuffer(data)
237
    if(!newData) {
238
        return false
239
    }
240
241
    try {
242
        fs.writeFileSync(filepath, newData, 'binary')
243
    } catch(error) {
244
        return error
245
    }
246
247
    return true
248
}
249
250
/**
251
 * @param {string} filepath - Filepath to file
252
 * @param {(error: Error) => void} fn - Function for async usage
253
 * @returns {void}
254
 */
255
function removeTagsAsync(filepath, fn) {
256
    fs.readFile(filepath, (error, data) => {
257
        if(error) {
258
            fn(error)
259
            return
260
        }
261
262
        const newData = removeTagsFromBuffer(data)
263
        if(!newData) {
264
            fn(error)
265
            return
266
        }
267
268
        fs.writeFile(filepath, newData, 'binary', (error) => {
269
            if(error) {
270
                fn(error)
271
            } else {
272
                fn(null)
273
            }
274
        })
275
    })
276
}
277
278
/**
279
 * Checks and removes already written ID3-Frames from a file
280
 * @param {string} filepath - Filepath to file
281
 * @param fn - (optional) Function for async usage
282
 * @returns {boolean|Error}
283
 */
284
function removeTags(filepath, fn) {
285
    if(isFunction(fn)) {
286
        return removeTagsAsync(filepath, fn)
287
    }
288
    return removeTagsSync(filepath)
289
}
290
291
function makeSwapParameters(fn) {
292
    return (a, b) => fn(b, a)
293
}
294
295
// The reorderParameter is a workaround because the callback function
296
// does not have a consistent interface between all the API functions.
297
// Ideally, all the functions should align with the promise style and
298
// always have the result first and the error second.
299
// Changing this would break the current public API.
300
// This could be changed internally and swap the parameter in a light
301
// wrapper when creating the public interface and then remove in a
302
// version 1.0 later with an API breaking change.
303
function makePromise(
304
    fn,
305
    reorderParameters = fn => (a, b) => fn(a, b)
306
) {
307
    return new Promise((resolve, reject) => {
308
        fn(reorderParameters((error, result) => {
309
            if(error) {
310
                reject(error)
311
            } else {
312
                resolve(result)
313
            }
314
        }))
315
    })
316
}
317
318
const PromiseExport = {
319
    create: (tags) => makePromise(create.bind(null, tags), makeSwapParameters),
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function create declared on line 104 does not use this.
Loading history...
320
    write: (tags, file) => makePromise(write.bind(null, tags, file)),
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function write declared on line 86 does not use this.
Loading history...
321
    update: (tags, file) => makePromise(update.bind(null, tags, file)),
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function update declared on line 171 does not use this.
Loading history...
322
    read: (file, options) => makePromise(read.bind(null, file, options)),
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function read declared on line 152 does not use this.
Loading history...
323
    removeTags: (filepath) => makePromise(removeTags.bind(null, filepath))
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function removeTags declared on line 284 does not use this.
Loading history...
324
}
325
326
module.exports = {
327
    Constants: ID3Definitions.Constants,
328
    create,
329
    write,
330
    update,
331
    read,
332
    removeTags,
333
    removeTagsFromBuffer,
334
    Promise: PromiseExport
335
}
336